﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net;
using System.IO;
using log4net;
using System.Net.Sockets;
using Repo7.BusinessLogic.Utils;

namespace Repo7.BusinessLogic.Network.FileTransfer
{
    /// <summary>
    /// Repository File Transfer
    /// </summary>
    public class RepoFileTransfer
    {
        #region Private Members

        /// <summary>
        /// Logger
        /// </summary>
        private static readonly ILog logger = LogManager.GetLogger(typeof(RepoFileTransfer));

        /// <summary>
        /// Server Thread
        /// </summary>
        private Thread ServerThread;

        #endregion

        #region Public Properties

        /// <summary>
        /// Thread signal.
        /// </summary>
        public static ManualResetEvent AllDoneThreadSignal;

        /// <summary>
        /// Server Port
        /// </summary>
        public IPEndPoint LocalEndPoint;

        /// <summary>
        /// Backlog Size
        /// </summary>
        public int BacklogSize;

        /// <summary>
        /// Repository location
        /// </summary>
        private static string LocalRepoLocation;

        /// <summary>
        /// File locks
        /// </summary>
        public static Dictionary<string, object> FilesLocks = new Dictionary<string,object>();

        #endregion

        #region Constructor
        
        /// <summary>
        /// Constructor
        /// </summary>
        public RepoFileTransfer(string localRepoLocation)
        {
            LocalRepoLocation = localRepoLocation; 
            AllDoneThreadSignal = new ManualResetEvent(false);
        }

        #endregion

        #region Public Methods

        public void StartListening(IPEndPoint commandEndPoint)
        {
            //Create a TCP/IP socket.
            Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // Bind the socket to the local endpoint and listen for incoming connections.
            try
            {
                listener.Bind(commandEndPoint);
                listener.Listen(BacklogSize);
                ServerThread = new Thread(new ParameterizedThreadStart(AcceptConnectionsRoutine));
                ServerThread.Start(listener);
            }
            catch (Exception exception)
            {
                logger.Fatal(Messages.ErrorBindingSocketToLocalEndpoint, exception);
            }
        }

        /// <summary>
        /// Send a file to the IPEndPont
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="destination"></param>
        /// <param name="tryCount"></param>
        public void SendFile(string fileName, IPEndPoint destination)
        {
            FileTransferRequest ftr = new FileTransferRequest()
            {
                Destination = destination,
                FileName = fileName
            };

            ThreadPool.QueueUserWorkItem(new WaitCallback(SendFileWorker), ftr);
        }
        
        #endregion

        #region Private Methods

        /// <summary>
        /// Start Server
        /// </summary>
        private void AcceptConnectionsRoutine(object listener)
        {
            // Bind the socket to the local endpoint and listen for incoming connections.
            try
            {
                while (true)
                {
                    // Set the event no signaled state
                    AllDoneThreadSignal.Reset();

                    // Start an asynchronous socket to listen for connections.
                    logger.Info(Messages.RepoFTServerStarted);
                    (listener as Socket).BeginAccept(new AsyncCallback(AcceptCallback), listener);
                    //Wait for a connection to be made
                    AllDoneThreadSignal.WaitOne();
                }
            }
            catch (Exception exception)
            {
                logger.Fatal(Messages.ErrorBindingSocketToLocalEndpoint, exception);
            }

            logger.Info(Messages.RepoFTServerStopped);

        }

        /// <summary>
        /// Accept Socket Connection Callback
        /// </summary>
        /// <param name="asyncResult"></param>
        private static void AcceptCallback(IAsyncResult asyncResult)
        {
            // Signal the main thread to continue
            AllDoneThreadSignal.Set();

            // Get the socket that handles the client request.
            Socket listener = asyncResult.AsyncState as Socket;
            Socket handler = listener.EndAccept(asyncResult);
            
            //Create the state object
            AsyncFileTransferStateObject stateObject = new AsyncFileTransferStateObject();
            stateObject.WorkSocket = handler;
            handler.BeginReceive(stateObject.FileBuffer, 0, AsyncFileTransferStateObject.BufferSize, SocketFlags.None,
                new AsyncCallback(ReadCallback), stateObject);

        }

        private static void ReadCallback(IAsyncResult asyncResult)
        {
            //Retrieve the state object and the handler socket
            AsyncFileTransferStateObject stateObject = (AsyncFileTransferStateObject)asyncResult.AsyncState;

            Socket handlerSocket = stateObject.WorkSocket;

            //Read data from the client socket
            int bytesRead = handlerSocket.EndReceive(asyncResult);

            if (bytesRead > 0)
            {
                int offset = 0;
                //If message length does not have the size set, read it from the first bytes
                if (!stateObject.FileLength.HasValue)
                {
                    offset = stateObject.GetMessageParameters(RepoFileTransfer.LocalRepoLocation);        
                }

                //Store the data received so far
                stateObject.WriteBytesToFile(offset, bytesRead);

                //Test to see if the message is read
                if (!stateObject.IsMessageReceived())
                {
                    handlerSocket.BeginReceive(stateObject.FileBuffer, 0, AsyncFileTransferStateObject.BufferSize, SocketFlags.None,
                        new AsyncCallback(ReadCallback), stateObject);
                }
                else
                {
                    stateObject.Dispose();
                }

            }
        }

        /// <summary>
        /// Method for transferring files
        /// </summary>
        /// <param name="FileTransferRequest"></param>
        private void SendFileWorker(object transferDetails)
        {
            int timeout = 1000;
            int tryCount = 10;
            int bufferSize = 10240;
            FileTransferRequest transferRequest = transferDetails as FileTransferRequest;
            FileInfo fileInfo = new FileInfo(transferRequest.FileName);

            using (FileStream fileStream = fileInfo.OpenRead())
            using (TcpClient client = new TcpClient())
            {
                while (tryCount > 0)
                {
                    try
                    {
                        client.Connect(transferRequest.Destination);
                        var clientStream = client.GetStream();

                        //Put the file size in the first bytes sent
                        byte[] buff = new byte[bufferSize];
                        int bytesWroteToBuffer = 0;
                        
                        //Add File Name Size to the buff
                        byte[] fileNameSizeBytes = new byte[sizeof(int)];
                        char[] fileNameChars = fileInfo.Name.ToCharArray();
                        fileNameSizeBytes = BitConverter.GetBytes(fileNameChars.Length);
                        fileNameSizeBytes.CopyTo(buff, 0);
                        bytesWroteToBuffer += sizeof(int);

                        //Add File Name to the buff
                        byte[] fileNameBytes = new byte[fileInfo.Name.Length * sizeof(char)];
                        Buffer.BlockCopy(fileNameChars, 0, fileNameBytes, 0, fileNameBytes.Length);
                        fileNameBytes.CopyTo(buff, bytesWroteToBuffer);
                        bytesWroteToBuffer += fileNameBytes.Length;

                        //Add File Size to the buff
                        byte[] fileSizeBytes = BitConverter.GetBytes(fileInfo.Length);
                        fileSizeBytes.CopyTo(buff, bytesWroteToBuffer);
                        bytesWroteToBuffer += fileSizeBytes.Length;

                        //The rest of the buff => start adding the file
                        int bytesReadFromFile = fileStream.Read(buff, bytesWroteToBuffer, bufferSize - bytesWroteToBuffer);
                        clientStream.Write(buff, 0, bytesWroteToBuffer + bytesReadFromFile);
                        long bytesSent = bytesWroteToBuffer + bytesReadFromFile;

                        while (bytesSent < fileInfo.Length)
                        {
                            int bytesRead = fileStream.Read(buff, 0, bufferSize);
                            clientStream.Write(buff, 0, bytesRead);
                            bytesSent += bytesRead;
                        }

                        return;

                    }
                    catch (Exception exception)
                    {
                        logger.Error(Messages.RepoCommanderCouldNotSendCommand, exception);
                        --tryCount;
                    }
                    Thread.Sleep(timeout);
                }
            }
        }

        #endregion

    }
}
